/*
 * main.cpp
 *
 *  Created on: Oct 10, 2010
 *      Author: rino
 */

#include "glm.h"

#include <opencv/cv.h>
#include <opencv/highgui.h>
#include <GL/glut.h>
#include <cmath>
#include <cstdlib>
#include <cstdio>

#define NONE 0
#define ROTATE (1 << 0)
#define SELECT (1 << 1)
#define BUFSIZE 1024
#define FALSE 0
#define TRUE 1

using namespace std;

GLfloat scale = 1.;
int m_ROTSCALE = 60;
int SIZE = 700;
int mode = NONE;
GLMmodel* data = NULL;
GLfloat lastPoint[3] = { 0 };

GLuint selectBuf[BUFSIZE] = { 0 };
GLdouble cursorX, cursorY;
GLint hits;
GLMtriangle* triangle = NULL;

GLuint texture;

inline GLfloat dotProduct(GLfloat* u, GLfloat* v) {
	return u[0] * v[0] + u[1] * v[1] + u[2] * v[2];
}

GLfloat* crossProduct(GLfloat u[3], GLfloat v[3], GLfloat n[3]) {
	n[0] = u[1] * v[2] - u[2] * v[1];
	n[1] = u[2] * v[0] - u[0] * v[2];
	n[2] = u[0] * v[1] - u[1] * v[0];
	return n;
}

GLfloat* subtract(GLfloat v1[3], GLfloat v2[3], GLfloat diff[3]) {
	diff[0] = v1[0] - v2[0];
	diff[1] = v1[1] - v2[1];
	diff[2] = v1[2] - v2[2];
	return diff;
}

inline float magnitude(GLfloat v[3]) {
	return sqrtf(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
}

GLfloat* normalize(GLfloat v[3]) {
	float mag = magnitude(v);
	v[0] /= mag;
	v[1] /= mag;
	v[2] /= mag;
	return v;
}

GLfloat* windowToHemi(int x, int y, GLfloat v[3]) {
	float d;
	v[0] = (2.0 * x - SIZE) / SIZE;
	v[1] = (SIZE - 2.0 * y) / SIZE;
	v[2] = 0.0;
	d = magnitude(v);
	d = (d < 1.0) ? d : 1.0;
	v[2] = sqrtf(1.001 - d * d);
	normalize(v);
	return v;
}

int LoadGLTextures() // Load Bitmaps And Convert To Textures
{
	glEnable(GL_TEXTURE_2D);
	glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP); // Set The Texture Generation Mode For S To Sphere Mapping ( NEW )
	glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP); // Set The Texture Generation Mode For T To Sphere Mapping ( NEW )
	glEnable(GL_TEXTURE_GEN_S); // Enable Texture Coord Generation For S ( NEW )
	glEnable(GL_TEXTURE_GEN_T); // Enable Texture Coord Generation For T ( NEW )


	int Status = FALSE; // Status Indicator

	IplImage *TextureImage = NULL; // Create Storage Space For The Texture

	// Load The Bitmap, Check For Errors, If Bitmap's Not Found Quit
	if ((TextureImage = cvLoadImage("BG.bmp", CV_LOAD_IMAGE_COLOR))) // Reflection Texture (Spheremap)
	{
		Status = TRUE; // Set The Status To TRUE

		glGenTextures(1, &texture); // Create Three Textures (For Two Images)


		//			// Create Nearest Filtered Texture
		//			glBindTexture(GL_TEXTURE_2D, texture[loop]); // Gen Tex 0 And 1
		//			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		//			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		//			glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop]->width,
		//					TextureImage[loop]->height, 0, GL_RGB, GL_UNSIGNED_BYTE,
		//					TextureImage[loop]->imageData);
		//
		//			// Create Linear Filtered Texture
		//			glBindTexture(GL_TEXTURE_2D, texture[loop + 2]); // Gen Tex 2, 3 And 4
		//			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		//			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		//			glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop]->width,
		//					TextureImage[loop]->height, 0, GL_RGB, GL_UNSIGNED_BYTE,
		//					TextureImage[loop]->imageData);
		//
		//			// Create MipMapped Texture
		glBindTexture(GL_TEXTURE_2D, texture); // Gen Tex 4 and 5
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, TextureImage->width,
				TextureImage->height, 0, GL_RGB, GL_UNSIGNED_BYTE,
				TextureImage->imageData);
		//		gluBuild2DMipmaps(GL_TEXTURE_2D, 3, TextureImage->width,
		//				TextureImage->height, GL_RGB, GL_UNSIGNED_BYTE,
		//				TextureImage->imageData);

		if (TextureImage) // If Texture Exists
		{
			cvReleaseImage(&TextureImage); // Free The Image Structure
		}
	}

	return Status; // Return The Status
}

void init() {
	glShadeModel(GL_SMOOTH); // Set Smooth Shading
	glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // Background Color
	glClearDepth(1.0f); // Depth buffer setup
	glEnable(GL_DEPTH_TEST); // Enables Depth Testing
	glDepthFunc(GL_LEQUAL); // The Type Of Depth Test To Do
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Use perspective correct interpolation if available

	glMatrixMode(GL_PROJECTION); // Select The Projection Matrix
	//glLoadIdentity(); // Reset The Projection Matrix
	//glFrustum(-1, 1, -1, 1, -1, 1); // Define perspective projection frustum

	glOrtho(-1., 1., -1., 1., -1., 1.);

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	glEnable(GL_LIGHTING);
	float frontColor[] = { 1.f, 1.f, 1.f, 1.0f };

	glMaterialfv(GL_FRONT, GL_AMBIENT, frontColor);
	glMaterialfv(GL_FRONT, GL_DIFFUSE, frontColor);
	glMaterialfv(GL_FRONT, GL_SPECULAR, frontColor);
	glMaterialf(GL_FRONT, GL_SHININESS, 100);

	float lightDirection[] = { 2.0f, 0.0f, 1.0f, 0 };
	float ambientIntensity[] = { 0.1f, 0.1f, 0.1f, 1.0f };
	float lightIntensity[] = { 0.9f, 0.9f, 0.9f, 1.0f };
	float lightSpec[] = { 1.0f, 1.0f, 1.0f, 1 };
	glLightfv(GL_LIGHT0, GL_AMBIENT, ambientIntensity);
	glLightfv(GL_LIGHT0, GL_DIFFUSE, lightIntensity);
	glLightfv(GL_LIGHT0, GL_POSITION, lightDirection);
	glLightfv(GL_LIGHT0, GL_SPECULAR, lightSpec);
	glEnable(GL_LIGHT0);

	LoadGLTextures();
}

void startPicking() {
	GLint viewport[4];

	glSelectBuffer(BUFSIZE, selectBuf);

	glGetIntegerv(GL_VIEWPORT, viewport);

	glRenderMode(GL_SELECT);

	glInitNames();

	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();

	gluPickMatrix(cursorX, viewport[3] - cursorY, 5, 5, viewport);
	//	GLdouble ratio = (viewport[2] + 0.0) / viewport[3];
	//	gluPerspective(45, ratio, 0.1, 1000);
	//glOrtho(-1., 1., -1., 1., -1., 1.);
	glScalef(scale, scale, scale);
	glMatrixMode(GL_MODELVIEW);
}

void processHits2() {
	GLint i, j, numberOfNames;
	GLuint names, *ptr, maxZ, *ptrNames;

	ptr = (GLuint *) selectBuf;
	maxZ = 0;
	for (i = 0; i < hits; i++) {
		names = *ptr;
		ptr++;
		if (ptr[1] > maxZ) {
			numberOfNames = names;
			maxZ = ptr[1];
			ptrNames = ptr + 2;
		}

		ptr += names + 2;
	}
	if (numberOfNames > 0) {
		printf("You picked triangle  ");
		ptr = ptrNames;
		for (j = 0; j < numberOfNames; j++, ptr++) {
			printf("%d ", *ptr);
			triangle = &data->triangles[*ptr];
			triangle->removed = 0xdeadbeef;
		}
	} else
		printf("You didn't click a triangle!");
	printf("\n");
	fflush(stdout);
}

void stopPicking() {
	glMatrixMode(GL_PROJECTION);
	glPopMatrix();
	glMatrixMode(GL_MODELVIEW);
	glFlush();
	hits = glRenderMode(GL_RENDER);
	if (hits != 0) {
		processHits2();
	}
	mode = NONE;
}

void displayFunc(void) {
	glViewport(0, 0, SIZE, SIZE);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	if (mode == SELECT) {
		startPicking();
	}

	glmDraw(data, GLM_SMOOTH | GLM_TEXTURE);

	if (mode == SELECT)
		stopPicking();
	else {
		if (triangle) {
			GLfloat min[3] = { (1 << 16) };
			GLfloat max[3] = { -(1 << 16) };
			GLfloat maxDelta = -(1 << 16);
			GLfloat center[3] = { 0., 0., 0. };

			for (int i = 0; i < 3; i++) {
				GLfloat* v = &data->vertices[3 * triangle->vindices[i]];
				for (int j = 0; j < 3; j++) {
					if (v[j] < min[j])
						min[j] = v[j];
					if (v[j] > max[j])
						max[j] = v[j];
				}
			}

			for (int i = 0; i < 3; i++) {
				if (max[i] - min[i] > maxDelta)
					maxDelta = max[i] - min[i];
			}

			for (int i = 0; i < 3; i++) {
				GLfloat* v = &data->vertices[3 * triangle->vindices[i]];
				for (int j = 0; j < 3; j++) {
					center[j] += v[j];
				}
			}

			for (int i = 0; i < 3; i++) {
				center[i] /= 3;
			}

			glViewport(SIZE, 0, SIZE, SIZE);

			GLfloat rot_axis[3] = { 0. };
			GLfloat target_dir[3] = { 0., 0., -1. };
			GLfloat *normal = &data->facetnorms[3 * triangle->findex];
			GLfloat dot = dotProduct(normal, target_dir);

			float rot_angle = acos(dot) * 180 / M_PI;
			crossProduct(normal, target_dir, rot_axis);
			//normalize(rot_axis);

			printf("\nnormal= %f %f %f\nrot_axis= %f %f %f\nrot_angle= %f\n",
					normal[0], normal[1], normal[2], rot_axis[0], rot_axis[1],
					rot_axis[2], rot_angle);
			fflush(stdout);

			glMatrixMode(GL_PROJECTION);
			glPushMatrix();
			glLoadIdentity();
			//			glOrtho(center[0] - .5, center[0] + .5, center[1] - .5, center[1]
			//					+ .5, center[2] - .5, center[2] + .5);
			//glScalef(1 / maxDelta, 1 / maxDelta, 1 / maxDelta);
			//glOrtho(min[0], max[0], min[1], max[1], min[2], max[2]);
			//			glOrtho(center[0] - maxDelta / 2, center[0] + maxDelta / 2,
			//					center[1] - maxDelta / 2, center[1] + maxDelta / 2,
			//					center[2] - maxDelta / 2, center[2] + maxDelta / 2);
			//			glOrtho(center[0] - maxDelta / 2, center[0] + maxDelta / 2,
			//					center[1] - maxDelta / 2, center[1] + maxDelta, -1, 1);
			//			glOrtho(center[0] - maxDelta / 2, center[0] + maxDelta / 2,
			//					center[1] - maxDelta / 2, center[1] + maxDelta / 2,
			//					center[2] - maxDelta / 2, center[2] + maxDelta / 2);
			glMatrixMode(GL_MODELVIEW);
			glPushMatrix();
			glLoadIdentity();
			glRotatef(rot_angle, rot_axis[0], rot_axis[1], rot_axis[2]);

			glBegin(GL_TRIANGLES);
			glVertex3fv(&data->vertices[3 * triangle->vindices[0]]);
			glVertex3fv(&data->vertices[3 * triangle->vindices[1]]);
			glVertex3fv(&data->vertices[3 * triangle->vindices[2]]);
			glEnd();

			glMatrixMode(GL_PROJECTION);
			glPopMatrix();
			glMatrixMode(GL_MODELVIEW);
			glPopMatrix();
			glFlush();
		}
		glutSwapBuffers();
	}
}

void specialFunc(int key, int x, int y) {
	GLfloat objectXform[16];

	glGetFloatv(GL_MODELVIEW_MATRIX, objectXform);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	switch (key) {
	case GLUT_KEY_DOWN:
		if (glutGetModifiers() == GLUT_ACTIVE_SHIFT) {
			glMatrixMode(GL_PROJECTION);
			glScalef(.5, .5, .5);
			scale *= .5;
			glMatrixMode(GL_MODELVIEW);
		} else
			glTranslatef(0., -1., 0.);
		break;
	case GLUT_KEY_UP:
		if (glutGetModifiers() == GLUT_ACTIVE_SHIFT) {
			glMatrixMode(GL_PROJECTION);
			glScalef(2., 2., 2.);
			scale *= 2.;
			glMatrixMode(GL_MODELVIEW);
		} else
			glTranslatef(0., 1., 0.);
		break;
	case GLUT_KEY_LEFT:
		glTranslatef(-1., 0., 0.);
		break;
	case GLUT_KEY_RIGHT:
		glTranslatef(1., 0., 0.);
		break;
	}

	glMultMatrixf(objectXform);
	glutPostRedisplay();
}

void motionFunc(int x, int y) {
	if (!(mode & ROTATE))
		return;

	GLfloat direction[3];
	GLfloat rotAxis[3];
	float rot_angle;
	float velocity;
	GLfloat curPoint[3];
	GLfloat objectXform[16];

	windowToHemi(x, y, curPoint);
	subtract(curPoint, lastPoint, direction);
	velocity = magnitude(direction);

	if (velocity > 0.0001) {
		crossProduct(lastPoint, curPoint, rotAxis);
		rot_angle = velocity * m_ROTSCALE;
		glGetFloatv(GL_MODELVIEW_MATRIX, objectXform);
		glMatrixMode(GL_MODELVIEW);
		glLoadIdentity();
		glRotatef(rot_angle, rotAxis[0], rotAxis[1], rotAxis[2]);
		glMultMatrixf(objectXform);
		glutPostRedisplay();
	}

	lastPoint[0] = curPoint[0];
	lastPoint[1] = curPoint[1];
	lastPoint[2] = curPoint[2];
}

void mouseFunc(int button, int state, int x, int y) {
	if (button == GLUT_LEFT_BUTTON) {
		if (state == GLUT_DOWN) {
			mode = ROTATE;
			windowToHemi(x, y, lastPoint);
		} else {
			mode = NONE;
		}
	} else if (button == GLUT_RIGHT_BUTTON) {
		if (state == GLUT_DOWN) {
			mode = SELECT;
			cursorX = x;
			cursorY = y;
		}
		//		 else {
		//			mode = NONE;
		//		}
	}
	glutPostRedisplay();
}

int main(int argc, char** argv) {
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
	glutInitWindowSize(2 * SIZE, SIZE);
	glutInitWindowPosition(100, 100);
	glutCreateWindow("Virtual Trackball");

	init();

	glutDisplayFunc(displayFunc);
	glutMouseFunc(mouseFunc);
	glutMotionFunc(motionFunc);
	glutSpecialFunc(specialFunc);

	if (argc >= 2)
		data = glmReadOBJ(argv[1]);
	else
		data = glmReadOBJ("cow.obj");
	if (!data)
		exit(0);
	glmUnitize(data);
	glmFacetNormals(data);
	glmVertexNormals(data, 90.0);
	glmSpheremapTexture(data);

	glutMainLoop();

	glmDelete(data);
	return 0;
}
